package vg.services.data_base_manager.realization;


import java.util.ArrayDeque;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import vg.services.data_base_manager.data.graph.Attribute;
import vg.services.data_base_manager.data.graph.Edge;
import vg.services.data_base_manager.data.graph.Graph;
import vg.services.data_base_manager.data.graph.Vertex;
import vg.services.data_base_manager.data.header.AttributeHeader;
import vg.services.data_base_manager.data.header.EdgeHeader;
import vg.services.data_base_manager.data.header.GraphHeader;
import vg.services.data_base_manager.data.header.GraphModelHeader;
import vg.services.data_base_manager.data.header.VertexHeader;
import vg.services.data_base_manager.data.skeleton.GraphTreeElement;
import vg.services.data_base_manager.interfaces.GraphDataBaseFormat_1_0;
import vg.services.data_base_manager.interfaces.IGraphDataBase;

/**
 * This class realizes model, which uses SQL data base.
 * This model may cache graph elements in operating memory.
 *
 * @author Timur Zolotuhin (e-mail: tzolotuhin@gmail.com)
 */
public class SQLite4JavaCacheGraphDataBase implements IGraphDataBase {
	// define max for elements, which place in cache
	private static final int MAX_GRAPH_CACHE = 100;
	private static final int MAX_SUBGRAPH_CACHE = 1000;
	private static final int MAX_VERTEX_CACHE = 10000;
	private static final int MAX_EDGE_CACHE = 10000;
	private static final int MAX_ATTRIBUTE_CACHE = 5000;

	// cache for graph elements
	private Map<Integer, GraphModelHeader> cache_graph_models;
	private Map<Integer, GraphHeader> cache_graphs;
	private Map<Integer, VertexHeader> cache_vertices;
	private Map<Integer, EdgeHeader> cache_edges;
	private Map<Integer, AttributeHeader> cache_attributes;

	// counters
	private int graphModelIdCounter;
	private int graphIdCounter;
	private int vertexIdCounter;
	private int edgeIdCounter;
	private int attributeIdCounter;

	// database data
	private SQLite4JavaDataBase_1_0 dataBase;

	// graph structure data
    // all structures places in data base (or caches) but it need for optimization access time.
	private Map<Integer, String> vertexId2Name; // <VertexId, Name>
	private Map<Integer, String> edgeId2Name; // <EdgeId, Name>
    private Map<Integer, String> graphModelId2Name; // graph model id -> name
    private Map<Integer, String> graphId2Name; // graph id -> name

	private Map<Integer, Set<Integer>> graphId2EdgeIdSet; // <GraphId, Set<EdgeId>>
	private Map<Integer, Set<Integer>> graphId2VertexIdSet; // <GraphId, Set<VertexId>>

    private Map<Integer, Set<Integer>> graphModelId2RootGraphIdSet; // graph model id -> root graph id set

    private BiMap<Integer, Integer> vertexId2InnerGraphId;

	/**
	 * Constructor.
	 */
	public SQLite4JavaCacheGraphDataBase() {
		create_model();
	}
	
	@Override
	public synchronized int createGraphModelHeader(String name)  {
		GraphModelHeader gh = new GraphModelHeader(graphModelIdCounter++);
		gh.name = name;
		cache_graph_models.put(gh.id, gh);
		check_and_commit();

        // optimization
        graphModelId2Name.put(gh.id, gh.name);

        if (!graphModelId2RootGraphIdSet.containsKey(gh.id)) {
            graphModelId2RootGraphIdSet.put(gh.id, new HashSet<Integer>());
        }

        return gh.id;
	}
	
	@Override
	public synchronized int createGraphHeader(int graphModelId, boolean directed, boolean root) {
		GraphHeader graphHeader = new GraphHeader(graphIdCounter++, graphModelId, directed, root);
		cache_graphs.put(graphHeader.getId(), graphHeader);
		check_and_commit();

        // optimization
        if(!graphId2VertexIdSet.containsKey(graphHeader.getId())) {
            graphId2VertexIdSet.put(graphHeader.getId(), new HashSet<Integer>());
        }

        if (!graphId2EdgeIdSet.containsKey(graphHeader.getId())) {
            graphId2EdgeIdSet.put(graphHeader.getId(), new HashSet<Integer>());
        }

        if (root && graphModelId2RootGraphIdSet.containsKey(graphModelId)) {
            graphModelId2RootGraphIdSet.get(graphModelId).add(graphHeader.getId());
        }

        return graphHeader.getId();
	}
	
	@Override
	public synchronized int createVertexHeader(int subgraph_id, int innerGraphId) {
		VertexHeader vh = new VertexHeader(vertexIdCounter++, subgraph_id);
		vh.link2inner_graph = innerGraphId;
		cache_vertices.put(vh.id, vh);
		check_and_commit();

        // optimization
        Set<Integer> vertexIdSet = graphId2VertexIdSet.get(vh.subgraph_id);
        if(vertexIdSet != null) {
            vertexIdSet.add(vh.id);
        }

        if (innerGraphId >= 0) {
            vertexId2InnerGraphId.put(vh.id, innerGraphId);
        }

        return vh.id;
	}
	
	@Override
	public synchronized int createEdgeHeader(int graphId, int sourceVertexId, int targetVertexId) {
        EdgeHeader eh = new EdgeHeader(edgeIdCounter++, graphId, sourceVertexId, targetVertexId);
        cache_edges.put(eh.id, eh);
        check_and_commit();

        // optimization
        Set<Integer> edgeIdSet = graphId2EdgeIdSet.get(eh.subgraph_id);
        if(edgeIdSet != null) {
            edgeIdSet.add(eh.id);
        }

        return eh.id;
	}
	
	@Override
	public synchronized int createEdgeAttributeHeader(int edgeId, String name, String strValue, int type) {
        if (name == null || strValue == null || !AttributeHeader.isTypeAvailable(type))
            return -1;

        // TODO need check that edgeId exist

        int attrId = -1;
        for (AttributeHeader attr : cache_attributes.values()) {
            if (attr.getOwnerType() == GraphDataBaseFormat_1_0.ATTRIBUTE_TABLE_EDGE_OWNER_TYPE &&
                    attr.getOwnerId() == edgeId &&
                    attr.getName().equals(name)) {
                attrId = attr.getId();
                break;
            }
        }

        if (attrId < 0) {
            List<Attribute> attributes = dataBase.getAttributes4Edge_List(edgeId);
            for (Attribute attr : attributes) {
                if (attr.getName() != null && attr.getName().equals(name)) {
                    attrId = attr.getStorableId();
                    break;
                }
            }
        }

        AttributeHeader attributeHeader;
        if (attrId < 0) {
            attributeHeader = AttributeHeader.createEdgeAttributeHeader(attributeIdCounter++, edgeId);
            attributeHeader.setName(name);
            attributeHeader.setValue(strValue, type);
            cache_attributes.put(attributeHeader.getId(), attributeHeader);
            update_attribute_in_skeleton(attributeHeader);
        } else {
            attributeHeader = AttributeHeader.createEdgeAttributeHeader(attrId, edgeId);
            attributeHeader.setName(name);
            attributeHeader.setValue(strValue, type);
            modifyAttributeHeader(attributeHeader);
        }

        check_and_commit();
        return attributeHeader.getId();
	}
	
	@Override
	public synchronized int createVertexAttributeHeader(int vertex_id, String name, String strValue, int type) {
        if (name == null || strValue == null || !AttributeHeader.isTypeAvailable(type))
            return -1;

        // TODO need check that edgeId exist

        int attrId = -1;
        for (AttributeHeader attr : cache_attributes.values()) {
            if (attr.getOwnerType() == GraphDataBaseFormat_1_0.ATTRIBUTE_TABLE_VERTEX_OWNER_TYPE &&
                    attr.getOwnerId() == vertex_id &&
                    attr.getName().equals(name)) {
                attrId = attr.getId();
                break;
            }
        }

        if (attrId < 0) {
            List<Attribute> attributes = dataBase.getAttributes4Vertex_List(vertex_id);
            for (Attribute attr : attributes) {
                if (attr.getName().equals(name)) {
                    attrId = attr.getStorableId();
                    break;
                }
            }
        }

        AttributeHeader attributeHeader;
        if (attrId < 0) {
            attributeHeader = AttributeHeader.createVertexAttributeHeader(attributeIdCounter++, vertex_id);
            attributeHeader.setName(name);
            attributeHeader.setValue(strValue, type);
            cache_attributes.put(attributeHeader.getId(), attributeHeader);
            update_attribute_in_skeleton(attributeHeader);
        } else {
            attributeHeader = AttributeHeader.createVertexAttributeHeader(attrId, vertex_id);
            attributeHeader.setName(name);
            attributeHeader.setValue(strValue, type);
            modifyAttributeHeader(attributeHeader);
        }

        check_and_commit();
        return attributeHeader.getId();
	}

    @Override
    public int createGraphAttributeHeader(int graphId, String name, String strValue, int type) {
        if (name == null || strValue == null || !AttributeHeader.isTypeAvailable(type))
            return -1;

        // TODO need check that edgeId exist

        int attrId = -1;
        for (AttributeHeader attr : cache_attributes.values()) {
            if (attr.getOwnerType() == GraphDataBaseFormat_1_0.ATTRIBUTE_TABLE_GRAPH_OWNER_TYPE &&
                    attr.getOwnerId() == graphId &&
                    attr.getName().equals(name)) {
                attrId = attr.getId();
                break;
            }
        }

        if (attrId < 0) {
            List<Attribute> attributes = dataBase.getAttributes4Graph_List(graphId);
            for (Attribute attr : attributes) {
                if (attr.getName() != null && attr.getName().equals(name)) {
                    attrId = attr.getStorableId();
                    break;
                }
            }
        }

        AttributeHeader attributeHeader;
        if (attrId < 0) {
            attributeHeader = AttributeHeader.createGraphAttributeHeader(attributeIdCounter++, graphId);
            attributeHeader.setName(name);
            attributeHeader.setValue(strValue, type);
            cache_attributes.put(attributeHeader.getId(), attributeHeader);
            update_attribute_in_skeleton(attributeHeader);
        } else {
            attributeHeader = AttributeHeader.createGraphAttributeHeader(attrId, graphId);
            attributeHeader.setName(name);
            attributeHeader.setValue(strValue, type);
            modifyAttributeHeader(attributeHeader);
        }

        check_and_commit();
        return attributeHeader.getId();
    }

    @Override
	public synchronized Graph getSubGraph(List<Integer> vertexIds) {
		commit_all();
		return dataBase.getSubGraph(vertexIds);
	}
	
	@Override
	public synchronized Graph getGraph(int subGraphId) {
		commit_all();
		return dataBase.getGraph(subGraphId);
	}
	
	@Override
	public synchronized Edge getEdge(int edgeId) {
        commit_all();
		return dataBase.getEdge(edgeId);
	}
	
	@Override
	public synchronized Vertex getVertex(int vertexId) {
		commit_all();
		return dataBase.getVertex(vertexId);
	}
	
	@Override
	public synchronized Graph getSomeRootGraphByGraphModelId(int graphModelId) {
		commit_all();
		return dataBase.getAnyRootGraph(graphModelId);
	}

    @Override
    public synchronized Set<Integer> getAllRootGraphIds(int graphModelId) {
        return new HashSet<Integer>(graphModelId2RootGraphIdSet.get(graphModelId));
    }

    @Override
	public synchronized Attribute getAttribute(int attrId) {
		commit_all();
		return dataBase.getAttribute(attrId);
	}
	
	@Override
	public synchronized VertexHeader getVertexHeader(int vertexId) {
		VertexHeader vh = cache_vertices.get(vertexId);
		if(vh != null) {
			return vh.clone();
		} else {
			return dataBase.getVertexHeader(vertexId);
		}
	}
	
	@Override
	public synchronized AttributeHeader getAttributeHeader(int attributeId) {
		AttributeHeader vh = cache_attributes.get(attributeId);
		if(vh != null) {
			return vh.clone();
		} else {
			return dataBase.getAttributeHeader(attributeId);
		}
	}
	
	@Override
	public synchronized Set<Integer> getAllGraphModelIds() {
		return new HashSet<Integer>(graphModelId2RootGraphIdSet.keySet());
	}
	
	@Override
	public synchronized GraphModelHeader getGraphModelHeader(int graphModelId) {		
		GraphModelHeader gh = cache_graph_models.get(graphModelId);
		if(gh != null) {
			return gh.clone();
		} else {
			return dataBase.getGraphModelHeader(graphModelId);
		}		
	}
	
	@Override
	public synchronized GraphHeader getGraphHeader(int graphId) {
		GraphHeader gh = cache_graphs.get(graphId);
		if(gh != null) {
			return gh.clone();
		} else {
			return dataBase.getGraphHeader(graphId);
		}		
	}
	
	@Override
	public synchronized List<List<Object>> executeSQLRequest(String request) {
		check_and_commit();
		return dataBase.executeSQLRequest(request);
	}
	
	@Override
	public synchronized GraphTreeElement getGraphModelTree(int graphModelId) {
		return create_skeleton(graphModelId);
	}
	
	@Override
	public synchronized void close() {
		dataBase.close();
	}
	
	@Override
	public synchronized Set<Integer> getAllVertexIds4Graph(Graph graph) {
		Set<Integer> result_ids = new HashSet<Integer>();
		Queue<Integer> current_ids = new ArrayDeque<Integer>();
		
		Collection<Vertex> tmp_vert_list = graph.getVertices();
		for(Vertex buf_v : tmp_vert_list) {
			current_ids.add(buf_v.getStorableId());
		}
		
		while(!current_ids.isEmpty()) {
			int e = current_ids.poll();
			result_ids.add(e);
			// FIXME It is very slow part of code. Need fix it.
			VertexHeader vh = getVertexHeader(e);
			if(vh.link2inner_graph >= 0) {
				 Set<Integer> vertexIdSet = graphId2VertexIdSet.get(vh.link2inner_graph);
				 current_ids.addAll(vertexIdSet);
			}
		}
		
		return result_ids;
	}
	
	@Override
	public synchronized Set<Integer> getAllVertexIds4GraphModel(int graphModelId) {
		Set<Integer> rootVertexIdSet = getAllRootGraphIds(graphModelId);

        Set<Integer> resultIds = new HashSet<Integer>();
        Queue<Integer> currentIds = new ArrayDeque<Integer>();

        currentIds.addAll(rootVertexIdSet);

		while(!currentIds.isEmpty()) {
			int vertexId = currentIds.poll();
			resultIds.add(vertexId);

            Object innerGraphIdObj = vertexId2InnerGraphId.get(vertexId);

            if (innerGraphIdObj != null) {
                currentIds.addAll(graphId2VertexIdSet.get((Integer)innerGraphIdObj));
            }
		}
		
		return resultIds;
	}
	
//=============================================================================
//--------------Modifying methods----------------------------------------------
	@Override
	public synchronized boolean modifyGraphModelHeader(GraphModelHeader newGraphModelHeader) {
		GraphModelHeader old_graph_header = cache_graph_models.get(newGraphModelHeader.id);

		// optimization
        graphModelId2Name.put(newGraphModelHeader.id, newGraphModelHeader.name);

		if(old_graph_header != null) {
			cache_graph_models.put(newGraphModelHeader.id, newGraphModelHeader);
			return true;
		} else {
			return dataBase.modifyGraphModelHeader(newGraphModelHeader);
		}
	}
	
	@Override
	public synchronized boolean modifyVertexHeader(VertexHeader newVertexHeader) {
		VertexHeader old_vertex_header = cache_vertices.get(newVertexHeader.id);

        // optimization
        Set<Integer> oldVertexIdSet = graphId2VertexIdSet.get(old_vertex_header.subgraph_id);
        if(oldVertexIdSet != null) {
            oldVertexIdSet.remove(old_vertex_header.id);
        }

        Set<Integer> newVertexIdSet = graphId2VertexIdSet.get(newVertexHeader.subgraph_id);
        if(newVertexIdSet != null) {
            newVertexIdSet.add(newVertexHeader.id);
        }

        if (old_vertex_header.link2inner_graph >= 0) {
            vertexId2InnerGraphId.remove(old_vertex_header.id);
        }

        if (newVertexHeader.link2inner_graph >= 0) {
            vertexId2InnerGraphId.put(newVertexHeader.id,newVertexHeader.link2inner_graph);
        }

		if(old_vertex_header != null) {
			cache_vertices.put(newVertexHeader.id, newVertexHeader);
			return true;
		} else {
			return dataBase.modifyVertexHeader(newVertexHeader);
		}
    }
	
	@Override
	public synchronized boolean modifyAttributeHeader(AttributeHeader newAttributeHeader) {
		AttributeHeader old_attr_header = cache_attributes.get(newAttributeHeader.getId());
		update_attribute_in_skeleton(newAttributeHeader);
		if(old_attr_header != null) {
			cache_attributes.put(newAttributeHeader.getId(), newAttributeHeader);
			return true;
		} else {
			return dataBase.modifyAttributeHeader(newAttributeHeader);
		}
	}
	
//=============================================================================
//--------------OTHER METHODS--------------------------------------------------
	@Override
	public synchronized String generateTitle4Graph(Graph graph) {
		// Note: doesn't work if subgraph will empty and 
		// subgraph id will equals -1, that it returns null
		if(graph == null)
            return null;
		
		int graphId = graph.getStorableId();
		
		if(graphId < 0) {
			Collection<Vertex> v_list = graph.getVertices();
			if(v_list != null && !v_list.isEmpty()) {
                VertexHeader vh = dataBase.getVertexHeader(v_list.iterator().next().getStorableId());
                GraphHeader sg_header = (vh == null) ? null : dataBase.getGraphHeader(vh.subgraph_id);
				if(sg_header != null)
					graphId = sg_header.getId();
			}
		}
		
		// optimization 
		if(graphId >= 0) {
            if (graphId2Name.containsKey(graphId)) {
                return graphId2Name.get(graphId);
            }

			Integer vertexId = vertexId2InnerGraphId.inverse().get(graphId);
			if(vertexId == null) {
				for(Integer graphModelId : graphModelId2RootGraphIdSet.keySet()) {
                    Set<Integer> rootGraphIdSet = graphModelId2RootGraphIdSet.get(graphModelId);
                    if(rootGraphIdSet.contains(graphId)) {
						return graphModelId2Name.get(graphId);
					}
				}
			} else {
				return vertexId2Name.get(vertexId);
			}
		}
		return null;
	}
	
//=============================================================================
//--------------PRIVATE METHODS------------------------------------------------
	private void check_and_commit() {
		if(cache_graph_models.size() > MAX_GRAPH_CACHE) {
			dataBase.addGraphModelHeaders(cache_graph_models.values());
			cache_graph_models.clear();
		}
		
		if(cache_graphs.size() > MAX_SUBGRAPH_CACHE) {
			dataBase.addGraphHeaders(cache_graphs.values());
			cache_graphs.clear();
		}
		
		if(cache_vertices.size() > MAX_VERTEX_CACHE) {
			dataBase.addVertexHeaders(cache_vertices.values());
			cache_vertices.clear();
		}
		
		if(cache_edges.size() > MAX_EDGE_CACHE) {
			dataBase.addEdgeHeaders(cache_edges.values());
			cache_edges.clear();
		}
		
		if(cache_attributes.size() > MAX_ATTRIBUTE_CACHE) {
			dataBase.addAttributeHeaders(cache_attributes.values());
			cache_attributes.clear();
		}
	}
	
	private void commit_all() {
		dataBase.addGraphModelHeaders(cache_graph_models.values());
		cache_graph_models.clear();
		
		dataBase.addGraphHeaders(cache_graphs.values());
		cache_graphs.clear();
		
		dataBase.addVertexHeaders(cache_vertices.values());
		cache_vertices.clear();

		dataBase.addEdgeHeaders(cache_edges.values());
		cache_edges.clear();
		
		dataBase.addAttributeHeaders(cache_attributes.values());
		cache_attributes.clear();
	}
	
	private void create_model() {
		try {
			dataBase = new SQLite4JavaDataBase_1_0();

			// cache
			cache_graph_models = new HashMap<Integer, GraphModelHeader>();
			cache_graphs = new HashMap<Integer, GraphHeader>();
			cache_vertices = new HashMap<Integer, VertexHeader>();
			cache_edges = new HashMap<Integer, EdgeHeader>();
			cache_attributes = new HashMap<Integer, AttributeHeader>();

			// counters
			graphModelIdCounter = 0;
			graphIdCounter = 0;
			vertexIdCounter = 0;
			edgeIdCounter = 0;
			attributeIdCounter = 0;

			// optimization
			vertexId2Name = new HashMap<Integer, String>();
			edgeId2Name = new HashMap<Integer, String>();
            graphModelId2Name = new HashMap<Integer, String>();
            graphId2Name = new HashMap<Integer, String>();

            graphId2VertexIdSet = new HashMap<Integer, Set<Integer>>();
			graphId2EdgeIdSet = new HashMap<Integer, Set<Integer>>();
            graphModelId2RootGraphIdSet = new HashMap<Integer, Set<Integer>>();
            vertexId2InnerGraphId = HashBiMap.create();
		} catch (Throwable ex) {		
	    	//throw(new CoreException("[" + this.getClass().getName() + ".createModel] [FAIL] Exception = " + ex.getMessage(),EnumCriticalityException.FAILED));
		}
	}

	private void update_attribute_in_skeleton(AttributeHeader header) {
		if(header.getName().equals("name") && header.isStringType()) {
			switch(header.getOwnerType()) {
				case AttributeHeader.VERTEX_OWNER_TYPE: {
					vertexId2Name.put(header.getOwnerId(), header.getStringValue());
					break;
				}

				case AttributeHeader.EDGE_OWNER_TYPE: {
					edgeId2Name.put(header.getOwnerId(), header.getStringValue());
					break;
				}

                case AttributeHeader.GRAPH_OWNER_TYPE: {
                    graphId2Name.put(header.getOwnerId(), header.getStringValue());
                    break;
                }
			}
		}
	}
	
	private GraphTreeElement create_skeleton(int graphModelId) {
		GraphTreeElement rootGraphTreeElement = new GraphTreeElement();

        String graphModelName = graphModelId2Name.get(graphModelId);
        Set<Integer> rootGraphIdSet = getAllRootGraphIds(graphModelId);

        rootGraphTreeElement.vg_setGraphModel_id_tmp(graphModelId);
        rootGraphTreeElement.vg_setName(graphModelName);

        for (Integer rootGraphId : rootGraphIdSet) {
            GraphTreeElement graphTreeElement = new GraphTreeElement();
            graphTreeElement.vg_setGraph_id_tmp(rootGraphId);
            rootGraphTreeElement.vg_add(graphTreeElement);

            String name = graphId2Name.get(rootGraphId);
            graphTreeElement.vg_setName(name);
            create_skeleton_part(rootGraphId, graphTreeElement);
        }

        return rootGraphTreeElement;
	}
	
	private void create_skeleton_part(int graphId, GraphTreeElement curr) {
		Set<Integer> vertexIdSet = graphId2VertexIdSet.get(graphId);
		if(vertexIdSet != null) {
			for(Integer vertexId : vertexIdSet) {
				GraphTreeElement e = new GraphTreeElement();
				e.vg_setVertex_id(vertexId);
				e.vg_setName(vertexId2Name.get(vertexId));
                Integer innerGraphId = vertexId2InnerGraphId.get(vertexId);
				if (innerGraphId != null) {
					e.vg_setGraph_id_tmp(innerGraphId);
					create_skeleton_part(innerGraphId, e);
				}
				curr.vg_add(e);
			}
		}
	}
}
